#!/usr/bin/env python # -*- coding: utf-8 -*- """ ******************************************************************** * Part of the QGis-Plugin LinearReferencing: * Python-Script to convert linear-reference-data ******************************************************************** * Date : 2025-12-03 * Copyright : (C) 2023 by Ludwig Kniprath * Email : ludwig at kni minus online dot de ******************************************************************** this program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. ******************************************************************** """ # Note: stored on webserver as txt to be downloadable import numbers # imports not necessary if executed in Python-Console from qgis.core import QgsProject, QgsGeometry, QgsWkbTypes, QgsFeatureRequest, NULL ############################################################################################### # Data-Input-Area Start... # source-to-target-conversion, possible values: # Nabs => absolute natural statinings # Nfract => relative natural stationings # Mabs => vertex-M-based stationings source_lr_mode = "Nabs" target_lr_mode = "Nfract" # Data-Layer-Settings: # Name of Data-Layer dl_name = "PoL" # Name of ID-Field in Data-Layer, only used for error-messages dl_id_fn = "id" # Name of Join-Field to Reference-Layer dl_reference_fn = "reference_id" # Source-Field->Target-Field 1 dl_src_fn_1 = "Stationing" dl_tgt_fn_1 = "n_fract" # optional Source-Field->Target-Field 2 dl_src_fn_2 = "" dl_tgt_fn_2 = "" # Reference-Layer-Settings # Name of Reference-Layer rl_name = "Gewässer" # Name of ID-Field in Reference-Layer, used for Relation Data-Layer <-> Reference-Layer rl_id_fn = "lszm_id" # ...Data-Input-Area End # hit "[Run Script]" # Good Luck ;-) ############################################################################################### def nabs_to_nfract(rl_g: QgsGeometry, nabs: float) -> tuple: """nabs_to_nfract-calculation Args: rl_g (QgsGeometry) nfract (float) Returns: tuple: (float nabs, str error_msg) """ # Rev. 2025-12-04 if isinstance(nabs, numbers.Number): if rl_g.length() > 0: if nabs <= rl_g.length(): return nabs / rl_g.length(), None else: return None, "stationing_out_of_range" else: return None, "reference_geometry_empty" else: return None, "statoning_not_numeric" def nfract_to_nabs(rl_g: QgsGeometry, nfract: float) -> tuple: """nfract_to_nabs-calculation Args: rl_g (QgsGeometry) nfract (float) Returns: tuple: (float nabs, str error_msg) """ # Rev. 2025-12-04 if isinstance(nfract, numbers.Number): if rl_g.length() > 0: if 0 <= nfract <= 1: return rl_g.length() * nfract, None else: return None, "stationing_out_of_range" else: return None, "reference_geometry_empty" else: return None, "statoning_not_numeric" def nabs_to_nabs(rl_g: QgsGeometry, nabs: float) -> tuple: """nabs_to_nabs-"dummy-calculation" => value-check only Args: rl_g (QgsGeometry) nabs (float) Returns: tuple: (float nabs, str error_msg) """ # Rev. 2025-12-04 if isinstance(nabs, numbers.Number): if rl_g.length() > 0: if 0 <= nabs <= rl_g.length(): return nabs, None else: return None, "stationing_out_of_range" else: return None, "reference_geometry_empty" else: return None, "statoning_not_numeric" def nabs_to_mabs(rl_g: QgsGeometry, nabs: float) -> tuple: """nabs_to_mabs-calculation checks Geometry-Type, but not ST_IsValidTrajectory returns interpolated m-value of nabs-stationed vertex Args: rl_g (QgsGeometry) nabs (float) Returns: tuple: (float mabs, str error_msg) """ # Rev. 2025-12-04 linestring_m_wkb_types = [ QgsWkbTypes.LineStringM, QgsWkbTypes.LineStringZM, QgsWkbTypes.MultiLineStringM, QgsWkbTypes.MultiLineStringZM, ] if rl_g.wkbType() in linestring_m_wkb_types: if rl_g.length() > 0: if 0 <= nabs <= rl_g.length(): interpolated_point = rl_g.interpolate(nabs) if not interpolated_point.isEmpty(): return interpolated_point.constGet().m(), None else: return None, "interpolation_error" else: return None, "stationing_out_of_range" else: return None, "reference_geometry_empty" else: return None, "reference_geometry_not_m_enabled" def mabs_to_nabs(rl_g: QgsGeometry, mabs: float) -> tuple: """mabs_to_nabs-calculation checks Geometry-Type, but not ST_IsValidTrajectory returns interpolated nabs-stationing between Vertex-Pair having last_vertex.m() <= mabs <= current_vertex.m() Args: rl_g (QgsGeometry) mabs (float) Returns: tuple: (float nabs, str error_msg) """ # Rev. 2025-12-04 linestring_m_wkb_types = [ QgsWkbTypes.LineStringM, QgsWkbTypes.LineStringZM, QgsWkbTypes.MultiLineStringM, QgsWkbTypes.MultiLineStringZM, ] if rl_g.wkbType() in linestring_m_wkb_types: last_vertex = None n = 0 for current_vertex in rl_g.constGet().vertices(): if last_vertex is not None: if last_vertex.m() <= mabs <= current_vertex.m(): delta_n = last_vertex.distance(current_vertex) delta_m_rel = (mabs - last_vertex.m()) / ( current_vertex.m() - last_vertex.m() ) n += delta_n * delta_m_rel return n, None else: # calculated pythagorean distance between the vertices n += last_vertex.distance(current_vertex) last_vertex = current_vertex return None, "stationing_out_of_range" else: return None, "reference_geometry_not_m_enabled" def lr_convert( dl_name: str, dl_id_fn: str, dl_reference_fn: str, dl_src_fn_1: str, dl_tgt_fn_1: str, rl_name: str, rl_id_fn: str, source_lr_mode: str, target_lr_mode: str, dl_src_fn_2: str = None, dl_tgt_fn_2: str = None, ): """Convert stationings Args: dl_name (str): Name of Data-Layer dl_id_fn (str): Name of ID-Field in Data-Layer, used for error-messages dl_reference_fn (str): Name of Join-Source-Field in Data-Layer dl_src_fn_1 (str): Field-Name with stationings to be converted dl_tgt_fn_1 (str): target field for converted stationings rl_name (str): Name of Reference-Layer rl_id_fn (str): Name of Join-Target-Field in Referfence-Layer source_rl_mode (str): source-LR-Mode Nabs, Nfract, Mabs target_rl_mode (str): target-LR-Mode Nabs, Nfract, Mabs dl_src_fn_2 (str): optional Field-Name with stationings to be converted (LoL stationing to) dl_tgt_fn_2 (str): optional target field for converted stationings (LoL stationing to) """ # Rev. 2025-12-04 # Pre-Check arguments: try: dl = QgsProject.instance().mapLayersByName(dl_name)[0] except: print(f"Data-Layer '{dl_name}' not found") return # look for the necessary/optional fields in data-layer dl_id_fidx = dl.fields().lookupField(dl_id_fn) dl_reference_fidx = dl.fields().lookupField(dl_reference_fn) dl_src_fidx_1 = dl.fields().lookupField(dl_src_fn_1) dl_tgt_fidx_1 = dl.fields().lookupField(dl_tgt_fn_1) dl_src_fidx_2 = dl.fields().lookupField(dl_src_fn_2) dl_tgt_fidx_2 = dl.fields().lookupField(dl_tgt_fn_2) if dl_id_fidx < 0: print(f"Data-Layer-ID-Field '{dl_id_fn}' not found") return if dl_reference_fidx < 0: print(f"Data-Layer-Reference-Field '{dl_reference_fn}' not found") return if dl_src_fidx_1 < 0: print(f"Data-Layer-Source-Field 1 '{dl_src_fn_1}' not found") return if dl_tgt_fidx_1 < 0: print(f"Data-Layer-Target-Field 1 '{dl_tgt_fn_1}' not found") return if dl_src_fidx_1 == dl_tgt_fidx_1: print(f"source_field_1 / target_field_1 {dl_src_fn_1} / {dl_tgt_fn_1} equal") return # Flag-Var: convert two stationings f.e. for LoL-Data? calc_2 = False if dl_src_fidx_2 > 0 and dl_tgt_fidx_2 > 0: if dl_src_fidx_2 != dl_tgt_fidx_2: calc_2 = True else: print( f"source_field_2 / target_field_2 {dl_src_fn_2} / {dl_tgt_fn_2} equal" ) return try: rl = QgsProject.instance().mapLayersByName(rl_name)[0] except: print(f"Reference-Layer '{rl_name}' not found") return rl_id_fidx = rl.fields().lookupField(rl_id_fn) if rl_id_fidx < 0: print(f"Reference-Layer-ID-Field '{rl_id_fn}' not found") return if source_lr_mode not in ["Nabs", "Nfract", "Mabs"]: print( f"source_lr_mode '{source_lr_mode}' not implemented (valid: Nabs, Nfract or Mabs)" ) return if target_lr_mode not in ["Nabs", "Nfract", "Mabs"]: print( f"target_lr_mode '{target_lr_mode}' not implemented (valid: Nabs, Nfract or Mabs)" ) return if source_lr_mode == target_lr_mode: print(f"source_lr_mode/target_lr_mode equal") return try: was_editable = dl.isEditable() if not was_editable: dl.startEditing() dl.beginEditCommand("lr_convert!") error_count = 0 num_features = dl.featureCount() for dl_f in dl.getFeatures(): dl_fid = dl_f.id() dl_id = dl_f[dl_id_fidx] source_stationing_1 = dl_f[dl_src_fidx_1] # optional: if calc_2: source_stationing_2 = dl_f[dl_src_fidx_2] source_nabs_1 = None source_nabs_2 = None target_stationing_1 = NULL target_stationing_2 = NULL error_msg = None # query and check joined reference-feature dl_reference_id = dl_f[dl_reference_fidx] request = QgsFeatureRequest() request.setFilterExpression(f"\"{rl_id_fn}\" = '{dl_reference_id}'") rl_f = None try: # will raise exception, if request has no results => reference-feature not found rl_f = next(rl.getFeatures(request)) if rl_f and rl_f.isValid(): if rl_f.hasGeometry(): rl_g = rl_f.geometry() if rl_g.length() > 0: # reference-geometry valid! # first base-conversion to source_nabs_1/source_nabs_2 if source_lr_mode == "Nabs": source_nabs_1, error_msg = nabs_to_nabs( rl_g, source_stationing_1 ) if calc_2: source_nabs_2, error_msg = nabs_to_nabs( rl_g, source_stationing_2 ) elif source_lr_mode == "Nfract": source_nabs_1, error_msg = nfract_to_nabs( rl_g, source_stationing_1 ) if calc_2: source_nabs_2, error_msg = nfract_to_nabs( rl_g, source_stationing_2 ) elif source_lr_mode == "Mabs": source_nabs_1, error_msg = mabs_to_nabs( rl_g, source_stationing_1 ) if calc_2: source_nabs_2, error_msg = mabs_to_nabs( rl_g, source_stationing_2 ) # second conversion source_nabs_1/source_nabs_2 to target_stationing_1/target_stationing_2 with target_lr_mode if not error_msg: if target_lr_mode == "Nfract": target_stationing_1, error_msg = nabs_to_nfract( rl_g, source_nabs_1 ) if calc_2: target_stationing_2, error_msg = nabs_to_nfract( rl_g, source_nabs_2 ) elif target_lr_mode == "Mabs": target_stationing_1, error_msg = nabs_to_mabs( rl_g, source_nabs_1 ) if calc_2: target_stationing_2, error_msg = nabs_to_mabs( rl_g, source_nabs_2 ) elif target_lr_mode == "Nabs": target_stationing_1, error_msg = nabs_to_nabs( rl_g, source_nabs_1 ) if calc_2: target_stationing_2, error_msg = nabs_to_nabs( rl_g, source_nabs_2 ) else: error_msg = f"reference-feature with ID '{dl_reference_id}' with empty geometry" else: error_msg = f"reference-feature with ID '{dl_reference_id}' without valid geometry" else: error_msg = f"no valid reference-feature for ID '{dl_reference_id}'" except: error_msg = f"no reference-feature for ID '{dl_reference_id}'" # update feature, set to NULL if any error occured dl_f[dl_tgt_fidx_1] = target_stationing_1 if calc_2: dl_f[dl_tgt_fidx_2] = target_stationing_2 dl.updateFeature(dl_f) if error_msg: error_count += 1 print( f"conversion-error: {error_msg} for data-feature '{dl_id}' (fid #{dl_fid}), target-field(s) set to NULL..." ) dl.endEditCommand() # commit edits but keep layer in previous edit-mode dl.commitChanges(not was_editable) if error_count > 0: print( f"{num_features} features in '{dl_name}' updated with {error_count} errors, some target-field(s) set to NULL, check result-table ..." ) else: print(f"{num_features} features in '{dl_name}' updated without errors...") except Exception as e: print(f"Exception occured: '{e}'") dl.rollBack() # convert stationings with settings defined above: lr_convert( dl_name, dl_id_fn, dl_reference_fn, dl_src_fn_1, dl_tgt_fn_1, rl_name, rl_id_fn, source_lr_mode, target_lr_mode, dl_src_fn_2, dl_tgt_fn_2 )